package kogukond;

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.util.Iterator;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.JTextField;
import javax.swing.JLabel;

import prefuse.Constants;
import prefuse.Display;
import prefuse.Visualization;
import prefuse.action.ActionList;
import prefuse.action.GroupAction;
import prefuse.action.ItemAction;
import prefuse.action.RepaintAction;
import prefuse.action.animate.ColorAnimator;
import prefuse.action.animate.PolarLocationAnimator;
import prefuse.action.animate.QualityControlAnimator;
import prefuse.action.animate.VisibilityAnimator;
import prefuse.action.assignment.ColorAction;
import prefuse.action.assignment.DataColorAction;
import prefuse.action.assignment.FontAction;
import prefuse.action.layout.CollapsedSubtreeLayout;
import prefuse.action.layout.graph.RadialTreeLayout;
import prefuse.activity.SlowInSlowOutPacer;
import prefuse.controls.ControlAdapter;
import prefuse.controls.DragControl;
import prefuse.controls.FocusControl;
import prefuse.controls.HoverActionControl;
import prefuse.controls.PanControl;
import prefuse.controls.ZoomControl;
import prefuse.controls.ZoomToFitControl;
import prefuse.data.Graph;
import prefuse.data.Node;
import prefuse.data.Table;
import prefuse.data.Tuple;
import prefuse.data.event.TupleSetListener;
import prefuse.data.query.SearchQueryBinding;
import prefuse.data.search.SearchTupleSet;
import prefuse.data.tuple.DefaultTupleSet;
import prefuse.data.tuple.TupleSet;
import prefuse.render.AbstractShapeRenderer;
import prefuse.render.DefaultRendererFactory;
import prefuse.render.EdgeRenderer;
import prefuse.render.LabelRenderer;
import prefuse.util.ColorLib;
import prefuse.util.FontLib;
import prefuse.util.ui.JFastLabel;
import prefuse.util.ui.JSearchPanel;
import prefuse.util.ui.JValueSlider;
import prefuse.util.ui.UILib;
import prefuse.visual.VisualGraph;
import prefuse.visual.VisualItem;
import prefuse.visual.expression.InGroupPredicate;
import prefuse.visual.sort.TreeDepthItemSorter;
import prefuse.action.filter.GraphDistanceFilter;

/**
 * Demonstration of a node-link tree viewer
 *
 * @version 1.0
 * @author <a href="http://jheer.org">jeffrey heer - enhanced by T Looy</a>
 */
public class KogukondRadialGraphView extends Display {
    private static final String tree = "tree";
    private static final String treeNodes = "tree.nodes";
    private static final String treeEdges = "tree.edges";
    private static final String linear = "linear";

    private LabelRenderer m_nodeRenderer;
    private EdgeRenderer m_edgeRenderer;

    private static final String m_label = "name";
    public static String sourceURL = "";

    // put test fields here so it will be accessible in the Query Buttons action listener's inline method
    static String[] genderChoices= {"", "M", "F"};
    static JComboBox genderQueryComboBox = new JComboBox(genderChoices);
    static JTextField genderQueryText = new JTextField(1);
    static JTextField fromAgeQueryText = new JTextField(2);
    static JTextField toAgeQueryText = new JTextField(3);
    static JTextField facetQueryText = new JTextField(20);

    static int hops = 7;
    final static GraphDistanceFilter separationFilter = new GraphDistanceFilter(tree, hops);

    public KogukondRadialGraphView(Graph g, String label) {
        super(new Visualization());

        // -- set up visualization and draw focus to the first node so degrees of separation will work --
        VisualGraph vg = m_vis.addGraph(tree, g);
        VisualItem f = (VisualItem)vg.getNode(0);
        m_vis.getGroup(Visualization.FOCUS_ITEMS).setTuple(f);
        m_vis.setInteractive(treeEdges, null, false);

        // -- set up renderers --
        m_nodeRenderer = new LabelRenderer(m_label);
        m_nodeRenderer.setRenderType(AbstractShapeRenderer.RENDER_TYPE_FILL);
        m_nodeRenderer.setHorizontalAlignment(Constants.CENTER);
        m_nodeRenderer.setRoundedCorner(8,8);
        m_edgeRenderer = new EdgeRenderer();

        DefaultRendererFactory rf = new DefaultRendererFactory(m_nodeRenderer);
        rf.add(new InGroupPredicate(treeEdges), m_edgeRenderer);
        m_vis.setRendererFactory(rf);

        // -- set up processing actions --

        // colors
        ItemAction nodeColor = new NodeColorAction(treeNodes);
        ItemAction textColor = new TextColorAction(treeNodes);
        m_vis.putAction("textColor", textColor);

        ItemAction edgeColor = new ColorAction(treeEdges,
                VisualItem.STROKECOLOR, ColorLib.rgb(200,200,200));

        FontAction fonts = new FontAction(treeNodes,
                FontLib.getFont("Tahoma", 10));
        fonts.add("ingroup('_focus_')", FontLib.getFont("Tahoma", 18));
        fonts.add("ingroup('_search_')", FontLib.getFont("Tahoma", 18));

        // recolor
        ActionList recolor = new ActionList();
        recolor.add(nodeColor);
        recolor.add(textColor);
        m_vis.putAction("recolor", recolor);

        // repaint
        ActionList repaint = new ActionList();
        repaint.add(recolor);
        repaint.add(new RepaintAction());
        m_vis.putAction("repaint", repaint);

        // animate paint change
        ActionList animatePaint = new ActionList(400);
        animatePaint.add(new ColorAnimator(treeNodes));
        animatePaint.add(new RepaintAction());
        m_vis.putAction("animatePaint", animatePaint);

        // create the tree layout action
        RadialTreeLayout treeLayout = new RadialTreeLayout(tree);
        m_vis.putAction("treeLayout", treeLayout);

        CollapsedSubtreeLayout subLayout = new CollapsedSubtreeLayout(tree);
        m_vis.putAction("subLayout", subLayout);

        // create the filtering and layout
        ActionList filter = new ActionList();
        filter.add(new TreeRootAction(tree));
        filter.add(fonts);
        filter.add(treeLayout);
        filter.add(subLayout);
        filter.add(textColor);
        filter.add(nodeColor);
        filter.add(edgeColor);
        m_vis.putAction("filter", filter);

        // animated transition
        ActionList animate = new ActionList(1250);
        animate.setPacingFunction(new SlowInSlowOutPacer());
        animate.add(new QualityControlAnimator());
        animate.add(new VisibilityAnimator(tree));
        animate.add(new PolarLocationAnimator(treeNodes, linear));
        animate.add(new ColorAnimator(treeNodes));
        animate.add(new RepaintAction());
        m_vis.putAction("animate", animate);
        m_vis.alwaysRunAfter("filter", "animate");

        ActionList slider = new ActionList();
        slider.add(separationFilter);
        // slider.add(fill);
        slider.add(new ColorAction(treeNodes, VisualItem.STROKECOLOR, 0));
        slider.add(new ColorAction(treeNodes, VisualItem.TEXTCOLOR, ColorLib.rgb(0,0,0)));
        slider.add(new ColorAction(treeEdges, VisualItem.FILLCOLOR, ColorLib.gray(200)));
        slider.add(new ColorAction(treeEdges, VisualItem.STROKECOLOR, ColorLib.gray(200)));
        m_vis.putAction("slider", slider);
        m_vis.alwaysRunAfter("slider", "animate");


        // create the highlighter for query criteria responses (code from Prefuse.org documentation
        int[] palette = new int[] {
        	    ColorLib.rgb(255,255,0), ColorLib.rgb(190,190,255)
        	};
        DataColorAction fill = new DataColorAction(treeNodes, "highlight", Constants.NOMINAL, VisualItem.FILLCOLOR, palette);


        ActionList queryHighlighter = new ActionList();
        queryHighlighter.add(fill);
        queryHighlighter.add(fonts);
		m_vis.putAction("queryHighlighter", queryHighlighter);
		m_vis.alwaysRunAfter("filter", "queryHighlighter");
		m_vis.alwaysRunAfter("repaint", "queryHighlighter");
		m_vis.alwaysRunAfter("animatePaint", "queryHighlighter");

        // ------------------------------------------------

        // initialize the display
        setSize(600,600);
        setItemSorter(new TreeDepthItemSorter());
        addControlListener(new DragControl());
        addControlListener(new ZoomToFitControl());
        addControlListener(new ZoomControl());
        addControlListener(new PanControl());
        addControlListener(new FocusControl(1, "filter"));
        addControlListener(new HoverActionControl("repaint"));

        // ------------------------------------------------

        // filter graph and perform layout
        m_vis.run("filter");


        // maintain a set of items that should be interpolated linearly
        // this isn't absolutely necessary, but makes the animations nicer
        // the PolarLocationAnimator should read this set and act accordingly
        m_vis.addFocusGroup(linear, new DefaultTupleSet());
        m_vis.getGroup(Visualization.FOCUS_ITEMS).addTupleSetListener(
            new TupleSetListener() {
                public void tupleSetChanged(TupleSet t, Tuple[] add, Tuple[] rem) {
                    TupleSet linearInterp = m_vis.getGroup(linear);
                    if ( add.length < 1 ) return; linearInterp.clear();
                    for ( Node n = (Node)add[0]; n!=null; n=n.getParent() )
                        linearInterp.addTuple(n);
                }
            }
        );

        SearchTupleSet search = new KPrefixSearchTupleSet();
        m_vis.addFocusGroup(Visualization.SEARCH_ITEMS, search);
        search.addTupleSetListener(new TupleSetListener() {
            public void tupleSetChanged(TupleSet t, Tuple[] add, Tuple[] rem) {
                m_vis.cancel("animatePaint");
                m_vis.run("recolor");
                m_vis.run("animatePaint");
            }
        });
    }

    // ------------------------------------------------------------------------

    public static void main(String argv[]) {
        Graph graphML = null;
        GetGraphML getGraph = new GetGraphML();

        if ( argv.length > 0 ) {
            sourceURL = argv[0];
            graphML = getGraph.getGraphML(sourceURL);
        }
        else {
            graphML = getGraph.getGraphML();
        }

        UILib.setPlatformLookAndFeel();

        JFrame frame = new JFrame("k o g u k o n d - p r e f u s e  |  r a d i a l g r a p h v i e w");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        frame.setContentPane(demo(graphML));
        frame.pack();
        frame.setVisible(true);
    }


	// This method is used by the Applet version of the application
    public static JPanel demo(String inputSourceURL) {
    	sourceURL = inputSourceURL;
    	Graph graphML = null;
        GetGraphML getGraph = new GetGraphML();
        graphML = getGraph.getGraphML(sourceURL);
    	return demo(graphML);
    }

    public static JPanel demo(Graph g) {
    	// create a new radial tree view
        final KogukondRadialGraphView gview = new KogukondRadialGraphView(g, m_label);
        Visualization vis = gview.getVisualization();


        // create the main panel and add a split panel to it
        JPanel mainPanel = new JPanel(new BorderLayout());
        JSplitPane split = new JSplitPane();
        mainPanel.add(split);


        // add the graph panel and the control panel and add to the split panel
        JPanel graphPanel   	= new JPanel(new BorderLayout());
        JPanel genderQueryPanel = new JPanel(new FlowLayout());
        JPanel ageQueryPanel 	= new JPanel(new FlowLayout());
        JPanel facetQueryPanel	= new JPanel(new FlowLayout());
        JPanel buttonPanel	 	= new JPanel(new FlowLayout());
        JPanel searchPanel	 	= new JPanel(new FlowLayout());
        JPanel nodeDetailPanel	= new JPanel(new FlowLayout());
        Box controlBox 			= Box.createVerticalBox();
        Box queryCriteriaBox	= Box.createVerticalBox();
        Box nodeDetailBox 		= Box.createVerticalBox();
        queryCriteriaBox.setBorder(	BorderFactory.createTitledBorder("Query Criteria"));
        nodeDetailBox.setBorder(	BorderFactory.createTitledBorder("Node Details"));
        controlBox.add(queryCriteriaBox);
        controlBox.add(nodeDetailBox);
        queryCriteriaBox.add(genderQueryPanel);
        queryCriteriaBox.add(ageQueryPanel);
        queryCriteriaBox.add(facetQueryPanel);
        queryCriteriaBox.add(buttonPanel);
        queryCriteriaBox.add(searchPanel);
        nodeDetailBox.add(nodeDetailPanel);
        split.setLeftComponent(graphPanel);
        split.setRightComponent(controlBox);
        split.setOneTouchExpandable(true);
        split.setContinuousLayout(false);
        split.setDividerLocation(700);



        // build out the graph panel
        graphPanel.add(gview, BorderLayout.CENTER);
        Color BACKGROUND = Color.WHITE;
        Color FOREGROUND = Color.DARK_GRAY;
        UILib.setColor(graphPanel, BACKGROUND, FOREGROUND);


       	// build out the control panel's query fields
       	JLabel genderQueryLabel = new JLabel("Gender:");
        genderQueryPanel.add(genderQueryLabel);
        genderQueryPanel.add(genderQueryComboBox);
        JLabel fromAgeQueryLabel = new JLabel("Age Range: From ");
        ageQueryPanel.add(fromAgeQueryLabel);
        ageQueryPanel.add(fromAgeQueryText);
        JLabel toAgeQueryLabel = new JLabel("To ");
        ageQueryPanel.add(toAgeQueryLabel);
        ageQueryPanel.add(toAgeQueryText);
        JLabel facetQueryLabel = new JLabel("Facet: ");
        facetQueryPanel.add(facetQueryLabel);
        facetQueryPanel.add(facetQueryText);

        // build the degrees of separation slider
        final JValueSlider slider = new JValueSlider("Separation", 0, hops, hops);
        slider.addChangeListener(new ChangeListener() {
            public void stateChanged(ChangeEvent e) {
    			// System.out.println("In slider change event.");
                separationFilter.setDistance(slider.getValue().intValue());
                gview.getVisualization().run("slider");
            }
        });
        slider.setBackground(Color.WHITE);
        slider.setPreferredSize(new Dimension(300,30));
        slider.setMaximumSize(new Dimension(300,30));
        // TODO: Add slider back into the app if:
        // 1. we can make it work consistently
        // 2. We don't get Kogukond Graph View to work the way we want to
//        ageQueryPanel.add(slider);


        // build the buttons
        final JButton queryButton = new JButton("Query");
        buttonPanel.add(queryButton);
       	queryButton.setBackground(Color.WHITE);
       	queryButton.addActionListener(new ActionListener() {
        	public void actionPerformed(ActionEvent e) {
        		if (sourceURL == "") {
        			System.out.println("Query option only available in distributed mode - doing a refresh instead.");
        			refreshGraph(gview);
        		}
        		else {
    				// TODO: Build XML of query criteria to send to backend rather than embedded parms.
        			String queryCriteria =  "?gender="  + genderQueryComboBox.getSelectedItem()
					 +	"&fromAge=" + fromAgeQueryText.getText()
					 +	"&toAge=" 	+ toAgeQueryText.getText()
					 +  "&facets="	+ facetQueryText.getText();
        			queryGraph(gview, queryCriteria);
        		}
        	}
        });


       	// build out the control panel's buttons
        final JButton refreshButton = new JButton("Refresh");
        buttonPanel.add(refreshButton);
        refreshButton.setBackground(Color.WHITE);
       	refreshButton.addActionListener(new ActionListener() {
        	public void actionPerformed(ActionEvent e) {
        		refreshGraph(gview);
        	}
        });


        // build out the control panel's search panel
        SearchQueryBinding sq = new SearchQueryBinding(
             (Table)vis.getGroup(treeNodes), m_label,
             (SearchTupleSet)vis.getGroup(Visualization.SEARCH_ITEMS));
        final JSearchPanel search = sq.createSearchPanel();
        search.setShowResultCount(true);
        search.setBorder(BorderFactory.createEmptyBorder(5,5,4,0));
        search.setFont(FontLib.getFont("Tahoma", Font.PLAIN, 11));
        searchPanel.add(search);


        // build out the control panel's node details
    	final JFastLabel titleDetail = new JFastLabel("                 ");
        titleDetail.setPreferredSize(new Dimension(400, 25));
        titleDetail.setBorder(BorderFactory.createEmptyBorder(3,50,0,0));
        titleDetail.setFont(FontLib.getFont("Tahoma", Font.PLAIN, 16));
        nodeDetailPanel.add(titleDetail);
    	final JFastLabel genderDetail = new JFastLabel("                 ");
        genderDetail.setPreferredSize(new Dimension(400, 25));
        genderDetail.setBorder(BorderFactory.createEmptyBorder(3,50,0,0));
        genderDetail.setFont(FontLib.getFont("Tahoma", Font.PLAIN, 16));
        nodeDetailPanel.add(genderDetail);

        gview.addControlListener(new ControlAdapter() {
            public void itemEntered(VisualItem item, MouseEvent e) {
                if ( item.canGetString(m_label) )
                    titleDetail.setText(item.getString(m_label));
                if ( item.canGetString("gender") )
                	genderDetail.setText(item.getString("gender"));
            }
            public void itemExited(VisualItem item, MouseEvent e) {
                titleDetail.setText(null);
                genderDetail.setText(null);
            }
        });


        // all built, now let's draw it
        vis.run("draw");
        return mainPanel;
    }

    // ------------------------------------------------------------------------

    /**
     * Switch the root of the tree by requesting a new spanning tree
     * at the desired root
     */
    public static class TreeRootAction extends GroupAction {
        public TreeRootAction(String graphGroup) {
            super(graphGroup);
        }
        public void run(double frac) {
            TupleSet focus = m_vis.getGroup(Visualization.FOCUS_ITEMS);
            if ( focus==null || focus.getTupleCount() == 0 ) return;

            Graph g = (Graph)m_vis.getGroup(m_group);
            Node f = null;
            Iterator tuples = focus.tuples();
            while (tuples.hasNext() && !g.containsTuple(f=(Node)tuples.next()))
            {
                f = null;
            }
            if ( f == null ) return;
            g.getSpanningTree(f);
        }
    }

    /**
     * Set node fill colors
     */
    public static class NodeColorAction extends ColorAction {
        public NodeColorAction(String group) {
            super(group, VisualItem.FILLCOLOR, ColorLib.rgba(255,255,255,0));
            add("_hover", ColorLib.gray(220,230));
			/* The Node Color Action for search items is now controlled by a DataColorAction
			 *            add("ingroup('_search_')", ColorLib.rgb(255,190,190));
			*/
            add("ingroup('_focus_')", ColorLib.rgb(198,229,229));
        }

    } // end of inner class NodeColorAction

    /**
     * Set node text colors
     */
    public static class TextColorAction extends ColorAction {
        public TextColorAction(String group) {
            super(group, VisualItem.TEXTCOLOR, ColorLib.gray(0));
            add("_hover", ColorLib.rgb(255,0,0));
            add("ingroup('_search_')", ColorLib.rgb(255,0,0));
        }
    } // end of inner class TextColorAction

	private static void refreshGraph(final KogukondRadialGraphView gview) {
		GetGraphML getGraph = new GetGraphML();
		Graph graphML;
		if (sourceURL == "") {
				graphML = getGraph.getGraphML();
		}
		else {
				graphML = getGraph.getGraphML(sourceURL);
		}
		resetGraph(gview, graphML);
	}

	private static void queryGraph(final KogukondRadialGraphView gview, String queryCriteria) {
		GetGraphML getGraph = new GetGraphML();
		Graph graphML = getGraph.queryGraphML(sourceURL, queryCriteria);
		resetGraph(gview, graphML);
	}

	private static void resetGraph(final KogukondRadialGraphView gview, Graph graphML) {
		gview.getVisualization().removeGroup(tree);
		// Clear the _search_ group because the removeGroup(tree) won't do it
		// TODO: Why does the clear() make the graph to flash on refresh?
		gview.getVisualization().getGroup(Visualization.SEARCH_ITEMS).clear();
		VisualGraph vg = (VisualGraph)gview.getVisualization().addGraph(tree, graphML);

		// TODO: try this here to see if it works.  If so, figure out how to get it to work
		// when the app starts
        VisualItem f = (VisualItem)vg.getNode(0);
        gview.getVisualization().getGroup(Visualization.FOCUS_ITEMS).setTuple(f);

		// TODO: Is there a better way to do this?  Seems clunky to call the constructor to get rebinding done.
		new SearchQueryBinding(
				(Table)gview.getVisualization().getGroup(treeNodes), m_label,
				(SearchTupleSet)gview.getVisualization().getGroup(Visualization.SEARCH_ITEMS));
		gview.getVisualization().run("filter");
		gview.getVisualization().run("draw");
	}
} // end of class KogukondRadialGraphView
